<?php

/**
 * This plugin provides Authentication for a WebDAV server.
 *
 * It relies on a Backend object, which provides user information.
 *
 * Additionally, it provides support for:
 *  * {DAV:}current-user-principal property from RFC5397
 *  * {DAV:}principal-collection-set property from RFC3744
 *
   * Copyright © 2010 Jack Cleaver
   * 
   * This plugin provides functionality added by CalDAV (RFC 4791)
   * It implements new reports, and the MKCALENDAR method.
   *
   * This file is part of Slash.
   *
   * Slash is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * Slash is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with Slash.  If not, see <http://www.gnu.org/licenses/>.
 */

class Slash_DAV_Auth_Plugin extends Sabre_DAV_ServerPlugin
{

	protected $authBackend;


	private $server;

	/**
	 * The authentication realm.
	 *
	 * @var string
	 */
	private $realm;

	public function __construct(Sabre_DAV_Auth_Backend_Abstract $authBackend, $realm)
	{
		$this->authBackend = $authBackend;
		$this->realm = $realm;
		//parent::__construct($authBackend, $realm);
	}

	public function initialize(Sabre_DAV_Server $server)
	{
		$this->server = $server;
		//$this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),10);
		$this->server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'));
		$this->server->subscribeEvent('report',array($this,'report'));
	}

	public function getCurrentUser()
	{
		return $this->authBackend->getCurrentUser();
	}

	public function getUsers()
	{
		return $this->authBackend->getUsers();
	}

	public function afterGetProperties($href, &$properties) {

		if (array_key_exists('{DAV:}current-user-principal', $properties[404])) {
			if ($ui = $this->authBackend->getCurrentUser()) {
				$properties[200]['{DAV:}current-user-principal'] = new Slash_ACL_Property_HrefPrincipal($ui['uri']);
			} else {
				$properties[200]['{DAV:}current-user-principal'] = new Slash_ACL_Property_UnauthenticatedPrincipal();
			}
			unset($properties[404]['{DAV:}current-user-principal']);
		}
		if (array_key_exists('{DAV:}principal-collection-set', $properties[404])) {
			$properties[200]['{DAV:}principal-collection-set'] = new Slash_ACL_Property_Href('principals');
			unset($properties[404]['{DAV:}principal-collection-set']);
		}
		if (array_key_exists('{DAV:}supported-report-set', $properties[200])) {
			$properties[200]['{DAV:}supported-report-set']->addReport(array(
                '{DAV:}expand-property',
			));
		}


	}

	/**
	 * This method is called before any HTTP method and forces users to be authenticated
	 *
	 * @param string $method
	 * @throws Sabre_DAV_Exception_NotAuthenticated
	 * @return bool
	 */
	public function beforeMethod($method)
	{
		Slash_Debug::debug("Slash_DAV_Auth_Plugin beforeMethod: Calling backend authenticate()");
		$this->authBackend->authenticate($this->server, $this->realm);
	}

	/**
	 * This functions handles REPORT requests
	 *
	 * @param string $reportName
	 * @param DOMNode $dom
	 * @return bool|null
	 */
	public function report($reportName,$dom) {

		switch($reportName) {
			case '{DAV:}expand-property' :
				$this->expandPropertyReport($dom);
				return false;
			case '{DAV:}principal-property-search' :
				if ($this->server->getRequestUri()==='principals') {
					$this->principalPropertySearchReport($dom);
					return false;
				}
				break;
			case '{DAV:}principal-search-property-set' :
				if ($this->server->getRequestUri()==='principals') {
					$this->principalSearchPropertySetReport($dom);
					return false;
				}
				break;
				 
		}

	}

	/**
	 * The expand-property report is defined in RFC3253 section 3-8.
	 *
	 * This report is very similar to a standard PROPFIND. The difference is
	 * that it has the additional ability to look at properties containing a
	 * {DAV:}href element, follow that property and grab additional elements
	 * there.
	 *
	 * Other rfc's, such as ACL rely on this report, so it made sense to put
	 * it in this plugin.
	 *
	 * @param DOMElement $dom
	 * @return void
	 */
	protected function expandPropertyReport($dom) {

		$requestedProperties = $this->parseExpandPropertyReportRequest($dom->firstChild->firstChild);
		$depth = $this->server->getHTTPDepth(0);
		$requestUri = $this->server->getRequestUri();

		$result = $this->expandProperties($requestUri,$requestedProperties,$depth);

		$dom = new DOMDocument('1.0','utf-8');
		$dom->formatOutput = true;
		$multiStatus = $dom->createElement('d:multistatus');
		$dom->appendChild($multiStatus);

		// Adding in default namespaces
		foreach($this->server->xmlNamespaces as $namespace=>$prefix) {

			$multiStatus->setAttribute('xmlns:' . $prefix,$namespace);

		}

		foreach($result as $entry) {

			$entry->serialize($this->server,$multiStatus);

		}

		$xml = $dom->saveXML();
		$this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
		$this->server->httpResponse->sendStatus(207);
		$this->server->httpResponse->sendBody($xml);

		// Make sure the event chain is broken
		return false;

	}

	/**
	 * This method is used by expandPropertyReport to parse
	 * out the entire HTTP request.
	 *
	 * @param DOMElement $node
	 * @return array
	 */
	protected function parseExpandPropertyReportRequest($node) {

		$requestedProperties = array();
		do {

			if (Sabre_DAV_XMLUtil::toClarkNotation($node)!=='{DAV:}property') continue;

			if ($node->firstChild) {

				$children = $this->parseExpandPropertyReportRequest($node->firstChild);

			} else {

				$children = array();

			}

			$namespace = $node->getAttribute('namespace');
			if (!$namespace) $namespace = 'DAV:';

			$propName = '{'.$namespace.'}' . $node->getAttribute('name');
			$requestedProperties[$propName] = $children;

		} while ($node = $node->nextSibling);

		return $requestedProperties;

	}

	/**
	 * This method expands all the properties and returns
	 * a list with property values
	 *
	 * @param array $path
	 * @param array $requestedProperties the list of required properties
	 * @param array $depth
	 */
	protected function expandProperties($path,array $requestedProperties,$depth) {

		$foundProperties = $this->server->getPropertiesForPath($path,array_keys($requestedProperties),$depth);

		$result = array();

		foreach($foundProperties as $node) {

			foreach($requestedProperties as $propertyName=>$childRequestedProperties) {

				// We're only traversing if sub-properties were requested
				if(count($childRequestedProperties)===0) continue;

				// We only have to do the expansion if the property was found
				// and it contains an href element.
				if (!array_key_exists($propertyName,$node[200])) continue;
				if (!($node[200][$propertyName] instanceof Sabre_DAV_Property_IHref)) continue;

				$href = $node[200][$propertyName]->getHref();
				list($node[200][$propertyName]) = $this->expandProperties($href,$childRequestedProperties,0);

			}
			$result[] = new Sabre_DAV_Property_Response($path, $node);

		}

		return $result;

	}

	protected function principalSearchPropertySetReport(DOMDocument $dom) {

		$searchProperties = array(
            '{DAV:}displayname' => 'display name'

            );

            $httpDepth = $this->server->getHTTPDepth(0);
            if ($httpDepth!==0) {
            	throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0');
            }

            if ($dom->firstChild->hasChildNodes())
            throw new Sabre_DAV_Exception_BadRequest('The principal-search-property-set report element is not allowed to have child elements');

            $dom = new DOMDocument('1.0','utf-8');
            $dom->formatOutput = true;
            $root = $dom->createElement('d:principal-search-property-set');
            $dom->appendChild($root);
            // Adding in default namespaces
            foreach($this->server->xmlNamespaces as $namespace=>$prefix) {

            	$root->setAttribute('xmlns:' . $prefix,$namespace);

            }

            $nsList = $this->server->xmlNamespaces;

            foreach($searchProperties as $propertyName=>$description) {

            	$psp = $dom->createElement('d:principal-search-property');
            	$root->appendChild($psp);

            	$prop = $dom->createElement('d:prop');
            	$psp->appendChild($prop);

            	$propName = null;
            	preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);

            	//if (!isset($nsList[$propName[1]])) {
            	//    $nsList[$propName[1]] = 'x' . count($nsList);
            	//}

            	// If the namespace was defined in the top-level xml namespaces, it means
            	// there was already a namespace declaration, and we don't have to worry about it.
            	//if (isset($server->xmlNamespaces[$propName[1]])) {
            	$currentProperty = $dom->createElement($nsList[$propName[1]] . ':' . $propName[2]);
            	//} else {
            	//    $currentProperty = $dom->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]);
            	//}
            	$prop->appendChild($currentProperty);

            	$descriptionElem = $dom->createElement('d:description');
            	$descriptionElem->setAttribute('xml:lang','en');
            	$descriptionElem->appendChild($dom->createTextNode($description));
            	$psp->appendChild($descriptionElem);


            }

            $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
            $this->server->httpResponse->sendStatus(200);
            $this->server->httpResponse->sendBody($dom->saveXML());

	}

	protected function principalPropertySearchReport($dom) {

		$searchableProperties = array(
            '{DAV:}displayname' => 'display name'

            );

            list($searchProperties, $requestedProperties) = $this->parsePrincipalPropertySearchReportRequest($dom);

            $uri = $this->server->getRequestUri();

            $result = array();

            $lookupResults = $this->server->getPropertiesForPath($uri, array_keys($searchProperties), 1);

            // The first item in the results is the parent, so we get rid of it.
            array_shift($lookupResults);

            $matches = array();

            foreach($lookupResults as $lookupResult) {

            	foreach($searchProperties as $searchProperty=>$searchValue) {
            		if (!isset($searchableProperties[$searchProperty])) {
            			throw new Sabre_DAV_Exception_BadRequest('Searching for ' . $searchProperty . ' is not supported');
            		}

            		if (isset($lookupResult[200][$searchProperty]) &&
            		mb_stripos($lookupResult[200][$searchProperty], $searchValue, 0, 'UTF-8')!==false) {
            			$matches[] = $lookupResult['href'];
            		}

            	}

            }

            $matchProperties = array();

            foreach($matches as $match) {

            	list($result) = $this->server->getPropertiesForPath($match, $requestedProperties, 0);
            	$matchProperties[] = $result;

            }

            $xml = $this->server->generateMultiStatus($matchProperties);
            $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
            $this->server->httpResponse->sendStatus(207);
            $this->server->httpResponse->sendBody($xml);

	}

	protected function parsePrincipalPropertySearchReportRequest($dom) {

		$httpDepth = $this->server->getHTTPDepth(0);
		if ($httpDepth!==0) {
			throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0');
		}

		$searchProperties = array();

		// Parsing the search request
		foreach($dom->firstChild->childNodes as $searchNode) {

			if (Sabre_DAV_XMLUtil::toClarkNotation($searchNode)!=='{DAV:}property-search')
			continue;

			$propertyName = null;
			$propertyValue = null;

			foreach($searchNode->childNodes as $childNode) {

				switch(Sabre_DAV_XMLUtil::toClarkNotation($childNode)) {

					case '{DAV:}prop' :
						$property = Sabre_DAV_XMLUtil::parseProperties($searchNode);
						reset($property);
						$propertyName = key($property);
						break;

					case '{DAV:}match' :
						$propertyValue = $childNode->textContent;
						break;

				}


			}

			if (is_null($propertyName) || is_null($propertyValue))
			throw new Sabre_DAV_Exception_BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue);

			$searchProperties[$propertyName] = $propertyValue;

		}

		return array($searchProperties, array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)));

	}
}
